menu
  Home  ==>  articles  ==>  prog_objet_composants  ==>  delphi_rtti   

RTTI Delphi - Run Time Type Information - John COLIBRI.

  • résumé : présentation et utilisation de la RTTI avec Delphi : RTTI Delphi 1, RTTI Delphi 2010: tRttiContext, exploration d'objets, architecture tRttiType, tRttiMember et tValue, example typiques : sérialisation d'objets .TXT, databinding, appels par Invoke, IOC et Dependency Injection. Diagrammes de classe UML
  • mots clé : RTTI Delphi 2010, tRttiContext, tRttiType, tRttiMember, tValue, sérialisation d'objets, databinding, Invoke, IOC, Dependency Injection, scripts, ORM, Make
  • logiciel utilisé : Windows XP personnel, Delphi Xe2
  • matériel utilisé : Pentium 2.800 Mhz, 512 Meg de mémoire, 250 Giga disque dur
  • champ d'application : Delphi 1 ("old RTTI") et Delphi 2010, Delphi XE, Delphi XE2, Delphi XE3 sur Windows
  • niveau : développeur Delphi
  • plan :


1 - Definition RTTI

RTTI (Run Time Type Information) permet d'analyser les types et des données à l'exécution. Par exemple nous pouvons récupérer la liste des propriétés d'un tButton (le type), ou la valeur de Form1.Left (la valeur de la propriété d'un objet).

Cette mécanique a été mise en place depuis Delphi 1, et permettait à l'époque la gestions des .DFM. Lors du lancement d'une application, l'.EXE charge le .DFM (stocké sous forme de resource dans l'.EXE) et construit les formes, les composants et initialise leurs propriétés.

Il s'agit en fait de "méta données" sur les types et les objets. Les langages comme Java ou C# on généralisé ces possibilités, qui sont appelées "introspection".

Pour les applications simples, nous n'avons pas besoin de RTTI.

RTTI est utilisé:

  • pas les constructeurs de frameworks, comme dUnit, Mock, ORM (Object Relational Mapping), protocoles de communications entre PC (Services Web, par exemple)
  • par les applications importantes pour lesquelles nous pouvons factoriser notre code en implémentant tout ou partie des frameworks cités ci-dessus, en permettant de généraliser certains traitements qui étaient spécifiques à chaque classe.



2 - Exemple de base RTTI

2.1 - Rtti Delphi 1

Nous utilisons régulièrement RTTI sous Delphi 1 pour récupérer le nom des énumérés en utilisant la fonction GetEnumName.

Voici un exemple simple ou nous affichons Align pour chaque contrôle d'une tForm:

  • la valeur de l'énuméré Align est calculé par

    Uses TypInfo;

    Function f_display_align(p_aligntAlign): String;
      Begin
        Result:= GetEnumName(TypeInfo(tAlign), Integer(p_align));
      End// f_display_align

  • et voici une utilisation simple:

    Procedure TForm1.display_align_Click(SenderTObject);
      Var l_control_indexInteger;
      Begin
        For l_control_index:= 0 To ControlCount- 1 Do
          With Controls[l_control_indexDo
            display(Format('%-21s %s', [Namef_display_align(Align)]));
      End// display_align_Click

    ou avec une récursion sur les tWinControls:

    Procedure display_control_alignment_recursive(p_levelintegerp_c_wincontroltWinControl);
      Var l_control_indexInteger;
          l_c_controltControl;
      Begin
        For l_control_index:= 0 To p_c_wincontrol.ControlCount- 1 Do
        Begin
          l_c_control:= p_c_wincontrol.Controls[l_control_index];
          With l_c_control Do
            display(Format('%'IntToStr(p_level)+ 's %-31s %s', [''Namef_display_align(Align)]));

          If l_c_control Is tWinControl Then
            display_control_alignment_recursive(p_level+ 2, l_c_control As tWinControl);
        End// for l_control_index
      End// display_control_alignment_recursive

    Procedure TForm1.display_align_recursive_Click(SenderTObject);
      Begin
        display_control_alignment_recursive(0, Self);
      End// display_align_recursive_Click

  • et voici un exemple de résultat:

    delphi_1_getenumname



2.2 - RTTI Delphi 2010

2.2.1 - Utilisation de base

La technique de base pour analyser les types et les valeurs des objets avec la nouvelle version de RTTI est
  • de définir une variable de type tRttiContext. Une variable de ce type est utilisée pour TOUTES les explorations de type Delphi 2010

  • d'appeler une fonction de tRttiContext pour récupérer les informations

    tRttiContext offre en effet les méthodes suivantes:

    Type TRttiContext=
             Record
               // ooo
               Function GetType(ATypeInfoPointer): TRttiTypeOverload;
               Function GetType(AClassTClass): TRttiTypeOverload;
               Function GetTypesTArray<TRttiType>;
               Function FindType(Const AQualifiedNamestring): TRttiType;
               Function GetPackagesTArray<TRttiPackage>;
            End// TRttiContext

  • tRttiType permet à son tour de récupérer les champs, méthodes et propriétés:

    Type TRttiType=
             Class(TRttiNamedObject)
               // ooo
               Function GetFieldsTArray<TRttiField>; Virtual;
               Function GetMethodsTArray<TRttiMethod>; OverloadVirtual;
               Function GetPropertiesTArray<TRttiProperty>; Virtual;

               Property TypeKindTTypeKind read GetTypeKind;       
               Property BaseTypeTRttiType read GetBaseType;
             End// TRttiType

  • et une fois que nous avons ces champs, méthodes et propriétés, nous pouvons examiner leurs caractéristiques


Pour récupérer les informations de type d'un tButton, par exemple, nous pouvons utiliser

Procedure TForm1.tbutton_rtti_Click(SenderTObject);
  Var l_rtti_contexttRttiContext;
      l_c_button_rtti_typetRttiType;
      l_c_rtti_fieldtRttifield;
  Begin
    l_c_button_rtti_type:= l_rtti_context.GetType(tButton);
    For l_c_rtti_field In l_c_button_rtti_type.GetFields Do
      display(l_c_rtti_field.ToString);
  End// tbutton_rtti_Click

dont voici le résultat:

tbutton_rttifields

l_c_rtti_field.ToString affiche "au mieux" les informations de chaque champ (le nom, le type, le décalage par rapport au début de l'objet)



2.3 - Explorateur d'Objets

Si nous partons d'un objet, nous pouvons utiliser son type, défini dans tObject.ClassType et à partir de là afficher ses champs, méthodes et propriétés.

De plus, comme nous avons des données en mémoire (un tObject a été créé), nous pouvons pour les champs et les propriétés récupérer leur valeur par des méthodes GetValue



L'utilisation classique est l'affichages des informations sur les composants d'une Forme: le noms des contrôles, leurs champs, méthodes et propriétés

Par exemple

  • nous posons sur Form1 quelques composants à analyser, CheckBox1, Edit1, Timer1 etc

  • nous remplissons une tListBox avec les Components de Form1 (dans notre cas, pour éviter les doublons sur les Panels, nous ne prenons que les composants dont le Name ne se termine pas par "_"

    Procedure TForm1.fill_object_list_Click(SenderTObject);
      Var l_component_index : Integer;
          l_component_nameString;
      Begin
        For l_component_index:= 0 To Self.ComponentCount -1 Do
        Begin
          l_component_name:= Components[l_component_index].Name;
          If l_component_name[Length(l_component_name)]<> '_' Then

          object_listbox_.Items.AddObject(l_component_nameComponents[l_component_index]);
        End// for l_component_index
      End// fill_object_list_Click

  • lorsque nous cliquons sur un objet de cette ListBox, nous remplissons trois autres ListBox avec les champs, méthodes et propriétés de ce contrôle:

    Function f_c_listbox_componenttComponent;
      Begin
        With Form1object_listbox_ Do
          Result:= Items.Objects[ItemIndexAs tComponent;
      End// f_c_listbox_component

    Procedure TForm1.object_listbox_Click(SenderTObject);
      Begin
        fill_field_list(f_c_listbox_componentfields_listbox_.Items);
        fill_method_list(f_c_listbox_componentmethods_listbox_.Items);
        fill_property_list(f_c_listbox_component,  properties_listbox_.Items);
      End// object_listbox_Click

  • nous affichons quelques informations sur les champs ainsi:

    Procedure fill_field_list(p_c_objecttObjectp_c_stringstStrings);
      Var l_rtti_contexttRttiContext;
          l_c_rtti_typetRttiType;
          l_c_rtti_fieldtRttifield;
          l_field_namel_field_type_namel_type_kind_namel_field_valueString;
      Begin
        p_c_strings.Clear;

        l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
        For l_c_rtti_field In l_c_rtti_type.GetFields Do
        Begin
          l_field_name:= l_c_rtti_field.Name;
          l_field_type_name:= l_c_rtti_field.FieldType.Name;
          l_type_kind_name:= f_display_type_kind(l_c_rtti_field.fieldType.TypeKind);

          If l_c_rtti_field.fieldType.TypeKind In
              [tkIntegertkFloat
               , tkChartkStringtkWChartkLStringtkWStringtkUString
               , tkEnumeration]
            Then l_field_value:= l_c_rtti_field.GetValue(p_c_object).ToString
            Else l_field_value:= '?' ;

          p_c_strings.Add(Format('%-19s %-18s %-15s %s',
              [l_field_namel_field_type_namel_type_kind_namel_field_value]));
        End// l_c_rtti_field
      End// fill_field_list

    et de façon similiaire pour les autres propriétés:

    Procedure fill_property_list(p_c_objecttObjectp_c_stringstStrings);
      Var l_rtti_contexttRttiContext;
          l_c_rtti_typetRttiType;
          l_c_rtti_propertytRttiProperty;
          l_property_namel_property_type_namel_type_kind_namel_property_valueString;
          l_displayString;
      Begin
        p_c_strings.Clear;

        l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
        For l_c_rtti_property In l_c_rtti_type.GetProperties Do
        Begin
          l_property_name:= l_c_rtti_property.Name;
          l_property_type_name:= l_c_rtti_property.PropertyType.Name;
          l_type_kind_name:= f_display_type_kind(l_c_rtti_property.PropertyType.TypeKind);

          If l_c_rtti_property.PropertyType.TypeKind In k_basic_type_kinds
            Then l_property_value:= l_c_rtti_property.GetValue(p_c_object).ToString
            Else l_property_value:= '?' ;

          l_display:= Format('%-25s %-16s %-15s %s',
              [l_property_namel_property_type_namel_type_kind_namel_property_value]);
          If p_c_strings.IndexOf(l_display)< 0
            Then p_c_strings.Add(l_display);
        End;
      End// fill_property_list

    ou les méthodes

    Procedure fill_method_list(p_c_objecttObjectp_c_stringstStrings);
      Var l_rtti_contexttRttiContext;
          l_c_rtti_typetRttiType;
          l_c_rtti_methodtRttiMethod;
      Begin
        p_c_strings.Clear;

        l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);

        For l_c_rtti_method In l_c_rtti_type.GetMethods Do
          p_c_strings.Add(l_c_rtti_method.ToString);
      End// fill_method_list




Notez que nous avons utilisé tRttiType.TypeKind, qui est un énuméré, pour tester si la valeur est affichable simplement. En effet si la propriété est de type Record ou Classe, il faudrait récurser pour trouver les valeurs simple (possible mais trop lourd pour cet exemple simple).

Les valeurs de tTypeKind sont:

TTypeKind = (tkUnknowntkIntegertkChartkEnumerationtkFloat,
    tkStringtkSettkClasstkMethodtkWChartkLStringtkWString,
    tkVarianttkArraytkRecordtkInterfacetkInt64tkDynArraytkUString,
    tkClassReftkPointertkProcedure);



Comme de plus tRttiType a un champ BaseType qui désigne la Classe ancêtre, nous pouvons récurser pour trouver la liste des ancêtres:

Procedure fill_method_list(p_c_objecttObjectp_c_stringstStrings);
  Var l_rtti_contexttRttiContext;
      l_c_rtti_typetRttiType;
      l_c_rtti_methodtRttiMethod;
  Begin
    p_c_strings.Clear;

    l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);

    For l_c_rtti_method In l_c_rtti_type.GetMethods Do
      p_c_strings.Add(l_c_rtti_method.ToString);
  End// fill_method_list



Voici l'affichage des champs de CheckBox1, par exemple:

tcheckbox_fields



Et pour terminer, comme les champs et les propriétés ont un GetValue, ils ont naturellement un SetValue :

  • un click sur une propriété de l'objet place la valeur de la propriété dans un tEdit

    Function f_property_string_value(p_c_objecttObjectp_property_displayString): String;
      Var l_indexinteger;
          l_property_nameString;
          l_rtti_contexttRttiContext;
          l_c_rtti_typetRttiType;
          l_c_rtti_propertytRttiProperty;
      Begin
        Result:= '?';

        l_index:= 1; l_property_name:= '';
        While (l_index<= Length(p_property_display)) And (p_property_display[l_index]<> ' ')  Do
        Begin
          l_property_name:= l_property_namep_property_display[l_index];
          inc(l_index);
        End;

        l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
        For l_c_rtti_property In l_c_rtti_type.GetProperties Do
        Begin
          If SameText(l_property_namel_c_rtti_property.Name)
            Then Begin
                If l_c_rtti_property.PropertyType.TypeKindtkInteger
                  Then Result:= l_c_rtti_property.GetValue(p_c_object).ToString
                Break;
              End;
        End;
      End// f_property_string_value

    Procedure TForm1.properties_listbox_Click(SenderTObject);
      Begin
        With properties_listbox_ Do
          property_value_edit_.Text:=
            f_property_string_value(f_c_listbox_objectItems[ItemIndex]);
      End// properties_listbox_Click

  • et le clic d'un bouton modifie cette propriété

    Procedure set_property_value(p_c_objecttObject;
        p_property_displayp_string_valueString);
      Var l_indexinteger;
          l_property_nameString;
          l_rtti_contexttRttiContext;
          l_c_rtti_typetRttiType;
          l_c_rtti_propertytRttiProperty;
      Begin
        l_index:= 1; l_property_name:= '';
        While (l_index<= Length(p_property_display)) And (p_property_display[l_index]<> ' ')  Do
        Begin
          l_property_name:= l_property_namep_property_display[l_index];
          inc(l_index);
        End;

        l_c_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);
        For l_c_rtti_property In l_c_rtti_type.GetProperties Do
        Begin
          If SameText(l_property_namel_c_rtti_property.Name)
            Then Begin
                If l_c_rtti_property.PropertyType.TypeKindtkInteger
                  Then l_c_rtti_property.SetValue(p_c_objectStrToInt(p_string_value));
                Break;
              End;
        End;
      End// set_property_value

    Procedure TForm1.set_property_value_Click(SenderTObject);
      Begin
        With properties_listbox_ Do
          set_property_value(f_c_listbox_objectItems[ItemIndex], property_value_edit_.Text);
      End// set_property_value_Click

  • dans notre exemple d'exécution, nous cliquons sur Left, qui a la valeur 8, et lorsque nous entrons la valeur 50, CheckBox1 se déplace :

    rtti_set_property_value




3 - Architecture RTTI

3.1 - Le Diagramme de Classe UML

Mes premières impressions pour RTTI étaient que
  • pour Delphi 1 il s'agissait d'une organisation ad hoc un peu bricolée, et que les développeurs n'ont pas arrêté d'étendre pour essayer d'extraire plus d'informations sur les types des classes
  • pour Delphi 2010, une mécanique très complète, mais ayant trop de détails. Utiliser RTTI sans partir d'une démo semblait assez osé.


Pour simplifier l'utilisation de RTTI, le mieux est de présenter directement les diagrammes de classe UML pour avoir une vue d'ensemble.

Voici l'organisation de base:

general_rtti_uml_class_diagram



Le premier objectif de RTTI est de modéliser les types Delphi, et essentiellement pour les Classes (pas les variables ou les procédures globales).

Voici une Classe simple:

Type c_person=
         Class
           m_ageInteger;
           m_first_nameString;
           m_salaryDouble;

           Constructor create_person(p_nameString;
                p_ageintegerp_salaryDouble);
           Function f_display_personString;

           Property FirstNameString Read m_first_name Write m_first_name;
         End// c_person

Les informations sur cette Classe devront comprendre

  • la Classe, c_person. Donc il faut un type TRttiInstanceType
  • puis ses champs: m_age. Comme c'est un entier, il faut une classe pour représenter cet entier (valeur min, max etc). Donc tRttiOrdinalType
  • pour m_name, nous pouvons souhaiter explorer le type de String (Ansi, WideString, UnicodeString etc)
  • il y a un Constructor et une Function. Il faut donc des informations sur le type de méthode (Constructor, Procedure, Function, Destructor). De plus est-ce une méthode de Classe, Virtual, pour une fonction, quel est le type de résultat. Et s'il y a des paramètres, il faut pouvoir les analyser. Paramètre valeur, Var, Const, sans type ?
  • et pour les Propertys, leur type, naturellement, mais aussi Read / Write, Default, index éventuel etc
En résumé, la hiérarchie RTTI doit donc modéliser toute la syntaxe des types Delphi.



3.2 - Technique d'extraction de type

Comme indiqué plus haut, pour avoir des informations sur un type (essentiellement les Classes):
  • nous définissons une variable de type tRttiContext
  • cette variable nous permet de récupérer un tRttiType
  • à partir de ce type, nous pouvons énumérer les champs, les méthodes et les propriétés


3.3 - RttiContext

tRttiContext est donc indispensable pour explorer les types. Ce type Record est essentiellement chargé de gérer un pool d'objet pour optimiser la construction et la consommation mémoire des objets tRtti_xxx.

Comme l'expliquait Barry KELLY (le concepteur de la nouvelle RTTI) :

  • il est fréquent que les objets RTTI ne soient pas nécessaires après des requêtes locales. Une gestion par Free serait fastidieuse
  • l'utilisation d'Interfaces avec libération automatique manque aussi de flexibilité
  • tRttiContext est la solution retenue permettant l'utilisation d'un Pool


Lorsque notre variable de type tRttiContext quitte la portée, les objets du cache qui avaient été créés en utilisant ce contexte sont libérés.



Comme c'est un Record, nous n'avons pas le besoin de le créer. Ni de le libérer

Néanmoins, si nous souhaitons créer dans une Procedure des objets tRtti_xxx, et les utiliser plus tard dans une autre Procedure, il faut que notre Record RTTI soit encore disponible.

Dans ce cas, nous pouvons gérer explicitement le contexte ainsi que les objets qu'il a aidé à construire :

Var g_c_rtti_contexttRttiContext;

  g_c_rtti_context:= tRttiContext.Create;

  // ooo

  g_c_rtti_context.Free;



Je dois avouer que des Records avec des Create et des Free me laissent assez perplexe, de même d'ailleurs qu'un Record muni de Procedures et Funcitions. Je préférais les Records qui étaient des données sans code. Enfin ...



3.4 - tRttiMember

Une fois récupéré le tRttiType d'une Classe, nous analysons le contenu de cette Classe. Donc les Classes RTTI d'analyse des membres d'une Classe sont essentiels.

Leur diagramme de Classe UML est le suivant

rttimember_uml_class_diagram



Quelques remarques:

  • nous avons remis ToString, même si la fonction étant nichée au niveau de tObject, elle est nécessairement présente partout. Néanmoins pour les membres RTTI, ToString effectue une analyze plus détaillée. Par exemple pour les méthodes, c'est l'en-tête complète qui est générée, comme l'a montré notre exemple ci-dessus.

    ToString est aussi une bon exemple d'analyse des objets RTTI. Voici le code pour les méthodes (indentation courtesy jcolibri):

    Function TRttiMethod.ToStringstring;
      Const k_method_kindArray[BoleanOf string = ('function ''procedure '); 
      Var l_parameter_listTArray<TRttiParameter>;
          l_parameter_indexInteger;
      Begin
        Result := '';
        If Not HasExtendedInfo
          Then Begin
              Result := '(basic) procedure ' + Name
              Exit;
            End;

        If IsClassMethod
          Then Result := Result + 'class '

        If IsConstructor
          Then Result := Result + 'constructor ' + Name 
          Else
            If IsDestructor
              Then Result := Result + 'destructor ' + Name 
              Else
                If MethodKind = mkOperatorOverload
                  Then Result := Result + 'operator ' + Name 
                  Else Result := Result + k_method_kind[ReturnType = Nil] + Name;

        l_parameter_list := GetParameters;
        If Length(l_parameter_list) > 0
          Then Result := Result + '(';
        For l_parameter_index := 0 To Length(l_parameter_list) - 1 Do
        Begin
          If l_parameter_index > 0
            Then Result := Result + '; ';
          Result := Result + l_parameter_list[l_parameter_index].ToString;
        End// for l_parameter_index

        If Length(l_parameter_list) > 0
          Then Result := Result + ')';

        If ReturnType <> Nil
          Then Result := Result + ': ' + ReturnType.Name;
      End// ToString

  • les Handle figurant à divers niveaux correspondent à des types très différents. Il s'agit manifestement d'un manque d'imagination pour trouver des noms :
    • pour tRttiObject, Handle est en fait un pPropInfo
    • pour tRttiIndexedProperty c'est un pArrayPropInfo
    • pour les méthodes, des pointeurs de VMT / IMT

  • pour tRttiObject d'ailleurs, le Handle pointe vers la structure de base du RTTI Delphi 1


3.5 - Valeurs des champs et propriétés - tValue

Si nous utilisons RTTI pour explorer la valeur des données d'un objet, tRttiField et tRttiProperty possèdent donc des GetValue et SetValue.

Ces méthodes manipulent un type tValue.

tValue a été créé pour gérer à la fois les valeurs et leur type.

Voici un résumé sous forme de diagramme de classe UML:

tvalue



tValue est un tuple de (valeur de donnée / information de type). tValue comporte un champ privé de type tValueData qui comporte effectivement un pTypeInfo et une zone de donnée contenant la valeur.



3.5.1 - tValue.Make

Make permet de construire un record tValue, et nous pouvons ensuite transférer les données de ce tValue vers des données standard.

Class Procedure Make(ABufferPointerATypeInfoPTypeInfoOut ResultTValue); OverloadStatic;
Class Procedure Make(AValueNativeIntATypeInfoPTypeInfoOut ResultTValue); OverloadStatic;



Voici un exemple:

Procedure TForm1.make_integer_Click(SenderTObject);
  Var l_integerInteger;
      l_integer_tvalueTValue;
      l_integer_from_tvalueInteger;
  Begin
    l_integer:= 1234;
    TValue.Make(@l_integerTypeInfo(Integer), l_integer_tvalue);

    display(l_integer_tvalue.ToString);
    l_integer_from_tvalue:= l_integer_tvalue.AsInteger;
  End// make_integer_Click

Notez que

  • nous avons extrait l'entier par AsString.
  • pour tValue, As n'extrait que les données du type annoncé. Nous n'aurions PAS pu stocker '123' et extraire AsInteger


Pour les données plus complexes, comme un Record, nous pouvons extraire les données par ExtractRawData (une sorte de Move):

Type t_priceRecord
                m_amountDouble;
                m_currencyString[9];
              End;

Procedure TForm1.extract_data_Click(SenderTObject);
  Var l_pricet_price;
      l_price_rawdatat_price;
      l_price_tvalueTValue;
  Begin
    l_price.m_amount:= 128.25;
    l_price.m_currency:= 'yen';
    TValue.Make(@l_priceTypeInfo(TRect), l_price_tvalue);

    l_price_tvalue.ExtractRawData(@l_price_rawdata);
    display(Format('%g %s', [l_price_rawdata.m_amountl_price_rawdata.m_currency]));
  End// extract_data_Click

Nous pouvons aussi construire le Record par Make et lui affecter des données;

Type t_string_9String[9];

Procedure TForm1.make_record_Click(SenderTObject);
  Var l_rtti_contextTRttiContext;
      l_price_tvalueTValue;
      l_tprice_rtti_typetRttiType;
      l_currencyString[9];
      l_pricet_price;
  Begin
    // -- create the empty record tValue
    TValue.Make(NilTypeInfo(TRect), l_price_tvalue);

    // -- set the values of the tValue
    l_tprice_rtti_type:= l_rtti_context.GetType(TypeInfo(t_price));
    l_tprice_rtti_type.GetField('m_amount').SetValue(
        l_price_tvalue.GetReferenceToRawData, 10);
(*
    l_currency:= 'dollar';
    l_rtti_context.GetType(TypeInfo(t_price)).GetField('m_currency').SetValue(
        l_price_tvalue.GetReferenceToRawData, t_string_9(l_currency));
*)

    // -- transfer to some standard t_price
    l_price_tvalue.ExtractRawData(@l_price);

    display(Format('%g %s', [l_price.m_amountl_price.m_currency]));
  End// make_record_Click

Notez que nous ne sommes pas parvenus à affecter une valeur au champ String (même en surtypant)



3.5.2 - tValue.From

Nous pouvons récupérer un tValue en utilisant différentes fonctions From.

Class Function FromVariant(Const ValueVariant): TValueStatic;
Class Function From<T>(Const ValueT): TValueStatic;
Class Function FromOrdinal(ATypeInfoPTypeInfoAValueInt64): TValueStatic;
Class Function FromArray(ArrayTypeInfoPTypeInfoConst ValuesArray Of TValue): TValueStatic;



Voici une extraction simple:

Procedure TForm1.from_type_Click(SenderTObject);
  Var l_pricet_price;
      l_price_tvalueTValue;
      l_price_rawdatat_price;
  Begin
    l_price.m_amount:= 128.25;
    l_price.m_currency:= 'yen';
    l_price_tvalue:= TValue.From<t_price>(l_price);

    display(f_display_TF(l_price_tvalue.IsType<t_price>));
    l_price_rawdata:= l_price_tvalue.AsType<t_price> ;
    display(Format('%g %s', [l_price_rawdata.m_amountl_price_rawdata.m_currency]));
  End// from_type_Click

et avec un variant:

Procedure TForm1.from_variant_Click(SenderTObject);
  Var l_variantVariant;
      l_tvalueTValue;
  Begin
    l_variant:= 'trial';
    l_tvalue:= TValue.FromVariant(l_variant);
    display(Format('%-15s %-15s %s',
        [f_display_type_kind(l_tvalue.Kind), l_tvalue.ToStringl_tvalue.AsTypevariant>]));

    l_variant:= 4321;
    l_tvalue:= TValue.FromVariant(l_variant);
    display(Format('%-15s %-15s %d',
        [f_display_type_kind(l_tvalue.Kind), l_tvalue.ToString, 3])); //l_tvalue.AsType< variant>]));
  End// from_variant_Click

ou un variant comme type générique

Procedure TForm1.from_T_Click(SenderTObject);
  Var l_variantVariant;
      l_tvalueTValue;
      l_integerInteger;
  Begin
    l_variant:= 'trial';
    l_tvalue:= TValue.Fromvariant> (l_variant);
    display(Format('%-15s %s',
        [f_display_type_kind(l_tvalue.Kind), l_tvalue.AsTypevariant>]));

    l_variant:= 4321;
    l_tvalue:= TValue.Fromvariant> (l_variant);
(*
    display(Format('%s',
        [l_tvalue.AsType< variant>]));

*)

    l_integer:= l_tvalue.AsTypevariant>;
    display(Format('%-15s %d',
        [f_display_type_kind(l_tvalue.Kind), l_integer]));
  End// store_variant_Click



3.5.3 - Résumé sur tValue

tValue
  • n'est pas prévu pour supporter
    • les opérateurs
    • les méthodes
  • n'est pas un remplacement du type Variant
  • permet les conversions
    • implicites chaque fois que c'est possible
    • a des conversions génériques pour les autres types (As_xxx)



4 - Exemples Rtti Delphi

4.1 - Sérialisation d'Objets

L'exemple le plus classique d'utilisation de RTTI est la sérialisation d'objets. Nous souhaitons transformer un objet mémoire en un série d'octets et pouvoir reconstruire l'objet à partir de ces octets. La sérialisation peut se faire vers un fichier .TXT, un fichier .XML, un tStream mémoire (pour être envoyé sur le réseau) etc.



Voici un exemple simple qui sérialise les Propertys d'un tObject:

Type c_serializer=
         Class
           Function f_serialize(p_c_objecttObject): String;
         End// c_serializer

Const // -- to know those, look at tValue.ToString
      k_not_serializable= [tkUnknown,
          tkSet,
          tkClasstkMethod,
          tkVarianttkArraytkRecordtkInterfacetkDynArray,
          tkClassReftkPointertkProcedure];

      k_toquote_kind= [tkChartkStringtkWChartkLStringtkWString,
          tkUString];

Function c_serializer.f_serialize(p_c_objecttObject): String;

  Function c_serializer.f_serialize_property(p_tvaluetValue): String;
    Begin
      If p_tvalue.IsEmpty
        Then Result:= 'Nil'
        Else Result:= p_tvalue.ToString;

      If p_tvalue.Kind In k_toquote_kind Then
        Result:= QuotedStr(Result);
    End// f_serialize_property

  Var l_c_result_listtStringList;
      l_c_object_typetRttiType;
      l_rtti_contexttRttiContext;
      l_c_rtti_propertytRttiProperty;
      l_tvaluetValue;

  Begin // f_serialize
    If p_c_objectNil
      Then Exit('');

    l_c_result_list:= tStringList.Create;

    l_c_object_type:= l_rtti_context.GetType(p_c_object.ClassType);

    For l_c_rtti_property In l_c_object_type.GetProperties Do
      If l_c_rtti_property.IsReadable And l_c_rtti_property.IsWritable
          And Not (l_c_rtti_property.PropertyType.Handle.Kind In k_not_serializable)
        Then Begin
            l_tvalue:= l_c_rtti_property.GetValue(p_c_object);
            l_c_result_list.Values[l_c_rtti_property.Name]:= f_serialize_property(l_tvalue);
          End;

    Result:= l_c_result_list.Text;
    l_c_result_list.Free;
  End// f_serialize

et la classe symétrique qui désérialise:

Type c_deserializer=
         Class
           Function f_deserialize_property(p_valueStringp_typeinfopTypeInfo): tValue;
           Procedure deserialize(p_stringStringp_c_objecttObject);
         End// c_deserializer

Procedure c_deserializer.deserialize(p_stringString;
    p_c_objecttObject);

  Function f_deserialize_property(p_valueString;
      p_typeinfopTypeInfo): tValue;
    Begin
      Case p_typeinfo.Kind Of
        tkIntegerResult:= StrToInt(p_value);
        tkInt64: ;
        tkEnumerationResult:= tValue.FromOrdinal(p_typeinfoSystem.TypInfo.GetEnumValue(p_typeinfop_value));
        tkFloatResult:= StrToFloat(p_value);

        tkChartkWChartkLStringtkWStringtkUString,
          tkStringResult:= p_value;
      End;
    End// f_deserialize_property

  Var l_c_input_listtStringList;
      l_rtti_contexttRttiContext;

      l_c_object_rtti_typetRttiType;
      l_list_indexinteger;
      l_c_rtti_propertytRttiProperty;
      l_property_namel_property_valueString;
      l_tvaluetValue;
      l_stringString;

  Begin // deserialize
    l_c_input_list:= tStringList.Create;
    l_c_input_list.Text:= p_string;

    l_c_object_rtti_type:= l_rtti_context.GetType(p_c_object.ClassType);

    For l_list_index:= 0 To l_c_input_list.Count- 1 Do
    Begin
      // -- "Caption='Ok'"
      // -- form the property name, get the tRttiProperty
      l_property_name:= l_c_input_list.Names[l_list_index];
      l_c_rtti_property:= l_c_object_rtti_type.GetProperty(l_property_name);

      // -- from the value, build a tValue
      If (SelfNilOr (l_c_rtti_property.PropertyType.Handle.Kind In k_not_serializable)
        Then Continue;

      // -- from the string value, get the tValue
      l_property_value:= l_c_input_list.ValueFromIndex[l_list_index];
      l_tvalue:= f_deserialize_property(l_property_valuel_c_rtti_property.PropertyType.Handle);

      // -- set the object property value
      If Not l_tvalue.IsEmpty
        Then Begin
            If l_c_rtti_property.PropertyType.Handle.Kind In k_toquote_kind
              Then Begin
                  l_string:= l_tvalue.AsString;
                  l_c_rtti_property.SetValue(p_c_objectAnsiDequotedStr(l_string''''))
                End
              Else l_c_rtti_property.SetValue(p_c_objectl_tvalue)
          End
        Else ; // display('value empty');
    End// for l_list_index

    l_c_input_list.Free;
  End// deserialize



Nous pouvons alors sérialiser un objet c_person

Procedure TForm1.create_and_serialze_person_Click(SenderTObject);
  Var l_c_personc_person;
      l_c_serializerc_serializer;
  Begin
    l_c_person:= c_person.create_person('matt', 45, 5461.33);
    l_c_serializer:= c_serializer.Create;
    g_serialized_person:= l_c_serializer.f_serialize(l_c_person);
    l_c_serializer.free;
    l_c_person.Free;
  End// create_and_serialze_person_Click

Procedure TForm1.deserialize_person_Click(SenderTObject);
  Var l_c_personc_person;
      l_c_deserializerc_deserializer;
  Begin
    l_c_person:= c_person.create_person('smith', 33, 4567.00);
    l_c_person:= c_person.create_person('', 0, 0);
    l_c_deserializer:= c_deserializer.Create;
    l_c_deserializer.deserialize(g_serialized_personl_c_person);
    display(l_c_person.f_display_person);
    l_c_deserializer.free;
    l_c_person.Free;
  End// deserialize_person_Click

et voici un exemple de sérialisation

rtti_serialization

ou un tButton, en changeant la valeurs de propriétés avant la désérialisation:

Var g_c_button_texttStringList;

Procedure TForm1.serialize_button_Click(SenderTObject);
  Var l_c_serializerc_serializer;
  Begin
    l_c_serializer:= c_serializer.Create;
    g_c_button_text:= tStringList.Create;

    g_c_button_text.Text:= l_c_serializer.f_serialize(Sender);

    display(g_c_button_text.Text);
  End// serialize_button_Click

Procedure TForm1.deserialize_button_Click(SenderTObject);
  Var l_c_deserializerc_deserializer;
  Begin
    With g_c_button_text Do
      Values['Left']:= '50';

    l_c_deserializer:= c_deserializer.Create;
    l_c_deserializer.deserialize(g_c_button_text.Textserialize_button_);
  End// deserialize_button_Click



4.2 - DataBinding

La "liaison des données" consiste à affecter à la propriété d'un tObject la même valeur d'un propriété d'un autre tObject.

C'est ce que font les composants dbEdit, dbGrid etc, en propageant les valeurs des tFields vers des propriétés d'affichage des contrôles.

Nous pouvons naturellement simplement affecter la valeur d'une propriété à un tEdit:

first_name_edit.Text:= g_c_person.FirstName

mais le but d'une mécanique de databinding est d'automatiser ces affectations.



De nombreuses solutions sont possibles

  • nous pouvons doter les objets d'une mécanique de liaison, mais cela oblige nos objets à descendre d'objets liables
  • plus général, nous pouvons créer des Interfaces, et utiliser des objets qui implémentent ces Interfaces
  • si nous ne souhaitons imposer aucune contrainte à nos objets, nous pouvons créer une liste des liaisons qui soit indépendante des objets
  • nous pouvons aussi utiliser une solution dissymétrique: un objet possède la liste des objets à mettre à jour. C'est cette solution que nous avons adoptée ici


Voici nos Classes:

Type c_origin_objectClass// forward

     c_binding_info=
         Class
           m_c_origin_objectc_origin_object;
           m_origin_property_nameString;
           m_c_target_objecttObject;
           m_target_property_nameString;

           Constructor Create(
               p_c_origin_objectc_origin_object;
               p_origin_property_nameString;
               p_c_target_objecttObject;
               p_target_property_nameString);

           Function f_bind_infoString;
         End// c_binding_info

     c_origin_object=
         Class(tObject)
             m_c_binding_info_listTList<c_binding_info> ;

             Constructor Create;
             Procedure add_target_object(
                 p_c_origin_objectc_origin_objectp_origin_property_nameString;
                 p_c_target_objecttObjectAControlPropertyString);
             Procedure update_target_values;
             Destructor DestroyOverride;
         End// c_origin_object

Notez que

  • comme l'objet origine contient la liste des objets destination, c_binding_info n'a pas réellement besoin de conserver une référence vers l'objet origine


Voici l'implémentation

// -- c_binding_info

Constructor c_binding_info.Create(
    p_c_origin_objectc_origin_objectp_origin_property_nameString;
    p_c_target_objecttObjectp_target_property_nameString);
  Begin
    Inherited Create;

    m_c_target_object:= p_c_target_object;
    m_target_property_name:= p_target_property_name;
    m_c_origin_object:= p_c_origin_object;
    m_origin_property_name:= p_origin_property_name;
  End// Create

Function c_binding_info.f_bind_infoString;
  Begin
    Result:=
        m_c_origin_object.ClassName'.'m_origin_property_name
        + '->'
        // + Control.Name+ '.'+ ControlPropertyName;
  End// f_bind_info

// -- c_origin_object

Constructor c_origin_object.Create;
  Begin
    Inherited;

    m_c_binding_info_list:= TList<c_binding_info>.Create;
  End// Create

Procedure c_origin_object.add_target_object(
    p_c_origin_objectc_origin_objectp_origin_property_nameString;
    p_c_target_objecttObjectAControlPropertyString);
  Begin
    m_c_binding_info_list.Add(
        c_binding_info.Create(
            Selfp_origin_property_name,
            p_c_target_objectAControlProperty));
  End// add_target_object

Procedure c_origin_object.update_target_values;
    // -- c_person.m_name -> tNameEdit.Text
  Var l_rtti_contexttRttiContext;
      l_c_origin_object_rtti_typetRttiType;
      l_c_origin_object_rtti_propertytRttiProperty;
      l_c_binding_infoc_binding_info;

      l_c_target_objecttObject;
      l_c_target_object_rtti_typetRttiType;
      l_c_target_object_rtti_propertytRttiProperty;

      l_origin_tvaluetValue;
  Begin
    l_rtti_context:= tRttiContext.Create;
    // -- scan the origin for the origin property names
    l_c_origin_object_rtti_type:= l_rtti_context.GetType(Self.ClassType);
    If l_c_origin_object_rtti_typeNil
      Then Begin
          display('Nil');
          Exit;
      End;

    For l_c_origin_object_rtti_property In l_c_origin_object_rtti_type.GetProperties Do
    Begin
      display(l_c_origin_object_rtti_property.ToString);
      For l_c_binding_info In m_c_binding_info_list Do
        If SameText(l_c_binding_info.m_origin_property_namel_c_origin_object_rtti_property.Name)
          Then Begin
              display('  same 'l_c_binding_info.m_origin_property_name);
              l_c_target_object:= l_c_binding_info.m_c_target_object;
              l_c_target_object_rtti_type:= l_rtti_context.GetType(l_c_target_object.ClassType);
              l_origin_tvalue:= l_c_origin_object_rtti_property.GetValue(Self);

              For l_c_target_object_rtti_property In l_c_target_object_rtti_type.GetProperties Do
                If SameText(l_c_binding_info.m_target_property_namel_c_target_object_rtti_property.Name)
                  Then Begin
                      display('    same 'l_c_binding_info.m_target_property_name);
                      l_c_target_object_rtti_property.SetValue(l_c_target_objectl_origin_tvalue);
                    End;
            End;
    End// for l_c_origin_object_rtti_property
    l_rtti_context.Free;
  End// update_target_values

Destructor c_origin_object.Destroy;
  Begin
    m_c_binding_info_list.Free;
    Inherited;
  End// Destroy



A titre d'exemple une c_person qui peut être lié à d'autres objets:

Type c_person// one "person"
         Class(c_origin_object)
           m_nameString;
           m_ageinteger;

           Constructor create_person(p_nameString;
               p_ageinteger);

           Procedure set_name(p_nameString);
           Procedure set_age(p_ageInteger);

           Property NameString Read m_name Write m_name;
           Property AgeInteger Read m_age Write set_age;
         End// c_person

Constructor c_person.create_person(p_nameStringp_ageinteger);
  Begin
    Inherited create;
    m_name:= p_name;
    m_age:= p_age;
  End// create_person

Procedure c_person.set_name(p_nameString);
  Begin
    m_name:= p_name;
    update_target_values
  End// set_name

Procedure c_person.set_age(p_ageInteger);
  Begin
    m_age:= p_age;
    update_target_values
  End// set_age



et voici un exemple avec liaison à un tEdit et un tTrackBar:

Var g_c_personc_personNil;

Procedure TForm1.create_person_Click(SenderTObject);
  Begin
    g_c_person:= c_person.create_person('joe', 41);
  End// create_person_Click

Procedure TForm1.bind_controls_Click(SenderTObject);
  Begin
    With g_c_person Do
    Begin
      add_target_object(g_c_person'Name'name_edit_'Text');
      add_target_object(g_c_person'Age'age_trackbar_'Position');

      update_target_values;
    End;
  End// bind_controls_Click

Procedure TForm1.set_random_age_Click(SenderTObject);
  Begin
    g_c_person.Age:= 20+ Random(75);
  End// set_random_age_Click

et un exemple d'exécution

rtti_databinding



Notez que

  • pour trouver la propriété origine, l'objet destination et la propriété destination, nous effectuons chaque fois des boucles For qui sont très peu efficaces
  • si nous souhaitions mémoriser les liens, il faudrait que le tRttiContext soit sauvegardé, soit dans l'objet origine, soit dans un objet global
  • nous n'avons sérialisé que les types simples (Integer, String etc). Si nous souhaitons sérialiser les Records ou les Classes, il faut récurser, et le problème devient plus volumineux
  • de nombreuses librairies incluent déjà la sérialisation, en mode .TXT ou en mode .XML


4.3 - RTTI Invoke

A partir d'un tRttiMethod, nous pouvons appeler Invoke, avec les paramètres éventuels.

Function Invoke(InstanceTObjectConst ArgsArray Of TValue): TValueOverload;
Function Invoke(InstanceTClassConst ArgsArray Of TValue): TValueOverload;
Function Invoke(InstanceTValueConst ArgsArray Of TValue): TValueOverload;

et la possibilité d'utiliser comme premier paramètre une référence de Classe correspond aux méthodes de Classe (comme le Constructor, entre autres).



4.3.1 - Appel de méthodes d'un objet

Voici un exemple où nous appelons quelques objets de notre Classe c_person:

Type c_person=
         Class
           m_nameString;
           m_ageinteger;
           m_salaryDouble;

           Constructor create_person(p_nameString;
               p_ageintegerp_salaryDouble);

           Function ToStringStringOverride;

           Destructor DestroyOverride;
         End// c_person

Constructor c_person.create_person(p_nameString;
    p_ageintegerp_salaryDouble);
  Begin
    Inherited create;
    m_name:= p_name;
    m_age:= p_age;
    m_salary:= p_salary;
  End// create_person

Procedure c_person.increment_age(p_deltaInteger);
  Begin
    Inc(m_agep_delta);
  End// increment_age

Function c_person.ToStringString;
  Begin
    Result:= Format('%-10s %2d %7.2f', [m_namem_agem_salary]);
  End// ToString

Function c_person.f_c_selfc_person;
  Begin
    Result:= Self;
  End// f_c_self

Procedure TForm1.invoke_methods_Click(SenderTObject);
  Var l_rtti_contextTRttiContext;
      l_c_personc_person;
      l_display_tvaluetValue;
      l_c_person_rtti_typetRttiType;
  Begin
    l_rtti_context:= TRttiContext.Create;
    l_c_person_rtti_type:= l_rtti_context.GetType(c_person);
    display(f_display_type_kind(l_c_person_rtti_type.TypeKind));

    l_c_person := c_person.Create;

    l_display_tvalue:= l_c_person_rtti_type.GetMethod('f_display_person').Invoke(l_c_person, []);
    display(l_display_tvalue.AsString);

    display(l_c_person_rtti_type.GetMethod('f_display_person').Invoke(l_c_person, []).AsString);

    l_c_person_rtti_type.GetMethod('increment_age').Invoke(l_c_person, [3]);
  End// invoke_methods_Click

Dans cet exemple, nous créons un objet c_person normal, et appelons quelques procédures en utilisant RTTI. Si déjà nous avons accès à c_person, il semble très alambiqué compliquer de passer par RTTI pour appeler une méthode §



4.3.2 - Utilisation de Invoke pour un Constructor

Néanmoins, nous pouvons utiliser RTTI pour invoquer le Constructor. Dans ce cas nous pouvons créer un tObject du bon type, puis appeler les méthodes de cet objet.

Voici le code;

Procedure TForm1.invoke_constructor_Click(SenderTObject);
  Var l_rtti_contextTRttiContext;
      l_c_rtti_typetRttiType;
      l_c_person_rtti_instance_typeTRttiInstanceType;
      l_c_class_reftClass;
      l_c_persontObject;
  Begin
    l_rtti_context:= TRttiContext.Create;
    l_c_rtti_type:= l_rtti_context.FindType('u_c_person_6.c_person');
    display(f_display_type_kind(l_c_rtti_type.TypeKind));
    l_c_person_rtti_instance_type:= l_c_rtti_type As TRttiInstanceType;

    l_c_class_ref:= l_c_person_rtti_instance_type.MetaclassType;

    l_c_person:= l_c_person_rtti_instance_type.GetMethod('create_person').Invoke(
          l_c_class_ref, ['joe', 15, 500]).AsType<c_person>;
    display(l_c_person.ToString);
  End// invoke_constructor_Click

et son exécution :

rtti_databinding



Notez que

  • l'identificateur c_person n'est mentionné nulle part, et pourtant nous avons pu appeler les méthodes sur un tObject
  • l'appel d'un Constructor par Invoque a les mêmes fonctionnalités qu'un appel de Constructor normal (appel de NewInstance, AfterConstruction etc)
Ce sont ces possibilités qui nous ouvrent la voie vers
  • la construction de scripts qui utilisent des Classes que nous avons construites
  • plus généralement toutes les transformations "nom de classe string -> objet", pour lesquelles nous utilisions en général la technique des références de Classe et les Constructors Virtuals
    Comme les Constructor Virtual sont une spécificité de Delphi, les autres langages utilisent en effet RTTI pour réaliser le même type de traitement
  • à part les scripts, ces conversions "nom de classe string -> objet" peuvent alors être utilisés pour
    • les plugins
    • les frameworks dUnit ou Mock
  • Invoke est aussi est nécessaire pour supporter les attributs personnalisés


4.3.3 - IOC - Inversion of Control

Voici, à titre d'exemple un exemple d'inversion de contrôle en utilisant une injection par le Constructor.



Supposons qu'une Classe c_consumer utilise d'une classe auxiliaire, mais que suivant le traitement elle fasse appel à différents descendants de cette classe auxiliaire (une classe utilise un client TCP/IP, mais suivant le cas ce sera un client Mail ou un client HTTP).

La technique de base est de

  • créer une Classe ancêtre, et que c_consumer n'utilise que cette Classe ancêtre. Au moment de l'exécution nous fournirons le client qui nous intéresse
  • créer une Classe abstraite, avec les mêmes constructions
  • ou finalement utiliser un Interface, et fournir, pour l'exécution, l'objet qui implémente l'Interface
Il est peu recommandé que c_consumer créé lui-même le bon client. Il vaut mieux que pour c_consumer nous utilisions "programming to the Interface", ce qui laisse à l'utilisateur de notre c_consumer le choix du client qu'il veut utiliser. Le contrôle a été inversé: ce n'est pas c_consumer qui crée le client concret, c'est le programme appelant qui lui injecte le client à utiliser.



Voici

  • notre Interface, la classe utilisatrice, c_consumer, et une Classe qui se charge de construire la bonne classe utilitaire:

    Type i_dependency=
             Interface
               ['{618030A2-DB17-4532-81D0-D5AA6F73DC66}']
               Procedure compute;
             End// i_dependency

         c_consumer=
             Class
               Private
                 m_i_dependencyi_dependency;
               Public
                 Constructor create_consumer(p_i_dependencyi_dependency);
                 Procedure do_compute;
             End// c_consumer

         c_dependency_injector=
             Class
               Public
                 Class Function f_i_dependency(p_qualified_dependency_class_namestring):
                       i_dependency;
             End// c_dependency_injector

    // -- c_consumer

    Constructor c_consumer.create_consumer(p_i_dependencyi_dependency);
      Begin
        m_i_dependency:= p_i_dependency;
      End// create_consumer

    Procedure c_consumer.do_compute;
      Begin
        If m_i_dependency<> Nil
          Then m_i_dependency.compute;
      End// do_compute

    // -- c_dependency_injector

    Class Function c_dependency_injector.f_i_dependency(p_qualified_dependency_class_namestring):
          i_dependency;
      Var l_rtti_contextTRttiContext;
          l_c_rtti_instance_typeTRttiInstanceType;
          l_c_class_reftClass;
          l_i_dependencyi_dependency;
      Begin
        l_c_rtti_instance_type:= l_rtti_context.FindType(p_qualified_dependency_class_name)
            As TRttiInstanceType;

        l_c_class_ref:= l_c_rtti_instance_type.MetaclassType;
        Result:= l_c_rtti_instance_type.GetMethod('create')
            .Invoke(l_c_class_ref, []).AsInterface As i_dependency
      End// f_i_dependency

  • puis un exemple de classe utilitaire:

    Type c_auxiliary_one=
             Class(TInterfacedObjecti_dependency)
               Public
                 Procedure compute;
             End// c_auxiliary_one

    Procedure c_auxiliary_one.compute;
      Begin
        display('Instance of type c_auxiliary_one');
      End// compute

  • et le programme final:

    Procedure TForm1.constructor_injection_Click(SenderTObject);
      Var l_i_dependencyi_dependency;
          l_c_consumerc_consumer;
      Begin
        l_i_dependency:= c_dependency_injector
            .f_i_dependency('u_c_auxiliary.c_auxiliary_one');

        l_c_consumer:= c_consumer.create_consumer(l_i_dependency);
        l_c_consumer.do_compute;
        l_c_consumer.Free;
      End// constructor_injection_Click

  • et un exemple d'exécution:

    rtti_ioc_constructor_injection



5 - Remarques RTTI Delphi 2010

5.1 - RTTI Delphi 1

Delphi 1 utilisait déjà RTTI, essentiellement pour linéariser dans le .DFM les Propertys publiques. Puis d'autres fonctionnalités ont été ajoutées :
  • TYPINFO.PAS (l'unité initiale)
    • n'utilise pas une mécanique objet
    • utilise des types bas niveau, des Record variants, des Pointer
    • est incomplet: pas les méta classes (Class Of) ni les Records
  • INTFINFO.PAS
    • est surtout optimisé pour Soap
  • OBJAUTO.PAS
    • apporte des information de type sur les méthodes
    • n'est pas objet
    • l'invocation se fait par des Variants
    • optimisé pour supporter le scripting COM


Le diagramme de Classe suivant montre bien cette mécanique non objet à base de Record variants (union libre) et de pointeurs en tous genres

old_delphi_rtti



Mentionnons néanmoins que nous pouvions déjà effectuer certains traitements comme la linéarisation des objets ou certaines invocations.



5.2 - RTTI Delphi 2010

L'avantage de la nouvelle mécanique est
  • une utilisation d'objets
  • l'inclusion de tous les champs
  • l'analyse des zones de visibilité autres que Public / Published
  • la sûreté des types ("Type Safe")
  • de permettre la métaprogrammation en utilisant les attributs (pas présenté ici)


5.3 - Volume de l'.EXE

Les informations de RTTI sont stockées dans l'.EXE, qui a donc une taille plus importante.

Pour évaluer cette augmentation, nous avons utilisé une simple Classe:

Type c_person=
         Class
           m_first_nameString;
           m_current_ageinteger;
           m_salary_minDouble;
           m_person_enumerationt_person_enumeration;

           Constructor create_person(p_nameString;
               p_ageintegerp_salaryDouble);
           Function f_display_personString;

           Property FirstNameString Read m_first_name Write m_first_name;
           Property CurrentAgeInteger Read m_current_age Write m_current_age;
           Property SalaryMinDouble Read m_salary_min Write m_salary_min;
         End// c_person

La taille de l'.EXE est de 1.500 K environ.

Un programme qui affiche les séquences ASCII de ce fichier a détecté:

 
          0015.1E4F PTypeInfo 
          0015.1EDE TDelegatedComparer 
          0015.1F00 PTypeInfo 
          0015.1FD8 MTDelegatedComparer 
          0015.201B PTypeInfo 
          0015.20A9 MTDelegatedComparer 
          0015.20EC PTypeInfo 
            
          0015.2474 m_first_name 
          0015.248C m_current_age 
          0015.24A5 m_salary_min 
          0015.24E1 create_person 
          0015.253F f_display_person 
          0015.25A2 c_person 
          0015.25B5 u_c_person 
          0015.25F7 FirstName 
          0015.261B CurrentAge 
          0015.2640 SalaryMin 
             
          0015.3063 Splitter1 
          0015.3100 Splitter1 
          0015.31AA exit_Click 
          0015.31BB clear_Click 
          0015.31CD FormCreate 
          0015.32AD exit_Click 
          0015.32E7 clear_Click 
          0015.3322 FormCreate 
          0015.34EF u_24_dump_rtti 
          0015.9195 u_c_person 
          0015.91A0 u_display_simple 
            
          0015.91C4 HelpIntfs 
          0015.91D5 RTLConsts 
          

et nous pouvons même effectuer un dump hexa de la zone RTTI de c_person en mémoire :

rtti_dump



Quelques remarques:

  • l'ensemble des types RTTI fait environ 19.000 lignes, soit environ 240K (sans les adresses hexa)
  • nous avons remarqué plusieurs pTypeInfo. En fait dans notre simple .EXE (une seule Classe tPerson), il y en a 91. Cette redondance a vraissemblablement été créée pour optimiser la vitesse plus que la place
  • lors de nos essais, nous avons essayé de voir si des informations RTTI étaient générées pour tous les Type (un Set Of, un Array etc.) En fait nous avons au début eu quelques difficultés car des informations sur les Classes n'apparaissaient pas toujours. En fait c'est le "Smart Linker" qui supprimait toutes les informations RTTI sur les Classes (ou autres types) qui étaient uniquement définis mais jamais utilisés (sous forme de variable, paramètre etc)
    Donc, si vous faites ce genre d'essai, pensez a utiliesr les types quelque part.


5.4 - Limitation du volume de RTTI dans l'.EXE

Pour limiter le volume des informations RTTI, nous pouvons placer au début du .DPR, avant toute clause Uses, les directives suivantes:

{$WEAKLINKRTTI ON}
{$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}

Ces directives

  • garantissent qu'aucune information RTTI ne sera générée pour nos propres unités ou les composants tiers compilés avec notre projet, sauf si nous avons explicitement exigé que ces informations soient générées
  • ne permettent pas de supprimer le RTTI de la RTL ou de la VCL
Compte tenu du volume RTTI constaté sur notre exemple précédent, cela peut représenter 20 % de la taille de l'.EXE (1.356 / 1.575)



5.5 - Ce que RTTI ne permet pas

Actuellement RTTI ne permet pas
  • l'analyze (invocation) de procédures globales
  • l'analyze de tous les types globaux (il permet le traitement des énumérés, des Classes, des Records et des champs / méthodes / propriétés contenus dans les Classes / Records de n'importe quel type)
Il s'agit donc d'une mécanique fondamentalement associées aux objets.



5.6 - Autres Exemples d'utilisation

Parmi les autres exemples mentionons
  • la possibilité d'étendre des types (ajouter des champs)
  • les mappings de tous genres, permettant par exemple
    • de créer des tDataSets à partir de Classes, ce qui est le départ vers les ORM
    • l'association automatique de champs d'un objet à des contrôles visuels (c_person.Age est automatiquement lié à age_edit_)


5.7 - Critiques sur RTTI Delphi 2010

Quelques critiques ont été adressées à la nouvelle version de RTTI
  • la complexité de son architecture.
  • le faible gain par rapport à ce qui était déjà possible avec le RTTI de Delphi 1



6 - Télécharger le code source Delphi

Vous pouvez télécharger:

Comme d'habitude:
  • nous vous remercions de nous signaler toute erreur, inexactitude ou problème de téléchargement en envoyant un e-mail à jcolibri@jcolibri.com. Les corrections qui en résulteront pourront aider les prochains lecteurs
  • tous vos commentaires, remarques, questions, critiques, suggestion d'article, ou mentions d'autres sources sur le même sujet seront de même les bienvenus à jcolibri@jcolibri.com.
  • plus simplement, vous pouvez taper (anonymement ou en fournissant votre e-mail pour une réponse) vos commentaires ci-dessus et nous les envoyer en cliquant "envoyer" :
    Nom :
    E-mail :
    Commentaires * :
     

  • et si vous avez apprécié cet article, faites connaître notre site, ajoutez un lien dans vos listes de liens ou citez-nous dans vos blogs ou réponses sur les messageries. C'est très simple: plus nous aurons de visiteurs et de références Google, plus nous écrirons d'articles.



7 - Références

Quelques références:

Le grand champion de RTTI Delphi 2010 est sans conteste Robert LOVE avec plus de 13 articles, parmi lesquels

Et sur ce site les articles suivants pour quelques techniques de base:
  • les Constructeurs Virtuels et les Références de classes (VIRTUAL CONSTRUCTORS, CLASS OF) permettent la séparation entre un projet et son fichier .EXE et des modules compilés séparément et liés à l'exécution. Par conséquent le point de départ pour les Framework Applicatifs et les Plugins
      John COLIBRI - 12 mar 2007
  • MyUtf : Environnement de Test Unitaire : cet environnement de test possède une structure très simple (deux listes), tout en offrant toutes les fonctionnalités des outils de test unitaire traditionnels. Voici un bon point de départ pour débuter dans ce domaine, comprendre la structure de tels outils ou comme point de départ pour améliorer ou transformer cet environnement en n'importe quel outil de traitement non invasif de parties de projets ou de librairies.
      Felix COLIBRI - 26 jan 2009
  • Les Génériques Delphi: exemple avec une tList<T>, création d'une pile, règles de compatibilité de type, génération du code, types pouvant être génériques, contraintes Interface, Class, héritage, Constructor. Exemple Observateur et Calculateur. Interfacees et conteneurs génériques de la Vcl
     John COLIBRI - 12 feb 2013



8 - L'auteur

John COLIBRI est passionné par le développement Delphi et les applications de Bases de Données. Il a écrit de nombreux livres et articles, et partage son temps entre le développement de projets (nouveaux projets, maintenance, audit, migration BDE, migration Xe_n, refactoring) pour ses clients, le conseil (composants, architecture, test) et la formation. Son site contient des articles avec code source, ainsi que le programme et le calendrier des stages de formation Delphi, base de données, programmation objet, Services Web, Tcp/Ip et UML qu'il anime personellement tous les mois, à Paris, en province ou sur site client.
Created: jan-04. Last updated: mar-2020 - 250 articles, 620 .ZIP sources, 3303 figures
Contact : John COLIBRI - Tel: 01.42.83.69.36 / 06.87.88.23.91 - email:jcolibri@jcolibri.com
Copyright © J.Colibri   http://www.jcolibri.com - 2001 - 2020
Retour:  Home  Articles  Formations  Développement Delphi  Livres  Pascalissime  Liens  Download
l'Institut Pascal

John COLIBRI

+ Home
  + articles_avec_sources
    + bases_de_donnees
    + web_internet_sockets
    + services_web_
    + prog_objet_composants
      – dump_interface
      – packages_delphi
      – ecriture_de_composant
      – c_list_of_double
      – interfaces_delphi
      – delphi_generics
      – delphi_rtti
    + office_com_automation
    + colibri_utilities
    + uml_design_patterns
    + graphique
    + delphi
    + outils
    + firemonkey
    + vcl_rtl
    + colibri_helpers
    + colibri_skelettons
    + admin
  + formations
  + developpement_delphi
  + présentations
  + pascalissime
  + livres
  + entre_nous
  – télécharger

contacts
plan_du_site
– chercher :

RSS feed  
Blog